Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable mode-specific transfers by storing mode information in transfers #6293

Conversation

VillePihlava
Copy link
Contributor

@VillePihlava VillePihlava commented Nov 29, 2024

Summary

This PR adds StreetMode information to transfers. This enables using mode-specific transfers during runtime.

Notes:

  • Instead of a collection of PathTransfers with mode information, this could also be done by using mappings from modes to transfers.
    • Depending on how this is done, this could maybe be cheaper in terms of memory?
    • In my tests the size of the graph has a similar size with both implementations. I prefer the implementation in this PR
  • The size of the graph can increase a lot when using a large dataset and when using CAR transfers.
    • When I tested building the graph with data from almost the entirety of Finland, the size of the graph file increased from 1.4G to 6.9G.
    • When using car transfers, because the transfers are calculated based on a range by duration there can be a lot of transfers.
    • The solution is to use CAR transfers only with relevant stops.
    • I will create a PR in the future for optionally limiting which stops can use transfers with cars.
    • Using an EnumSet was the cheapest way I could think of adding this functionality when having the information in PathTransfer.
  • For large datasets with many transfers, build duration can increase.
    • When I tested building the graph with data from almost the entirety of Finland, build duration doubled.
    • For less transfers, build duration does not increase as much.

Issue

Resolves a TODO comment about adding StreetMode information in the PathTransfer class.

Related to #5875

Unit tests

Added a new test to DirectTransferGeneratorTest.

Documentation

No documentation has been added.

Bumping the serialization version id

This changes how transfers are generated during the graph build.

@VillePihlava VillePihlava requested a review from a team as a code owner November 29, 2024 13:57
@VillePihlava
Copy link
Contributor Author

The tests will fail for CarPickupSnapshotTest. There is discussion related to changing the test in #6238. This PR should not be merged before that test has been changed and the tests pass.

@VillePihlava
Copy link
Contributor Author

Interestingly FlexIntegrationTest fails as well. I was able to locally run this test and all other tests except CarPickupSnapshotTest successfully, but I have noticed some flakiness related to running that test and running tests in general.

@VillePihlava
Copy link
Contributor Author

Interestingly now after making changes not related to functionality, the FlexIntegrationTest did not fail.

Copy link

codecov bot commented Dec 4, 2024

Codecov Report

Attention: Patch coverage is 78.48101% with 17 lines in your changes missing coverage. Please review.

Project coverage is 69.82%. Comparing base (69e6f64) to head (18215a9).
Report is 181 commits behind head on dev-2.x.

Files with missing lines Patch % Lines
...apping/StreetModeToTransferTraverseModeMapper.java 14.28% 5 Missing and 1 partial ⚠️
...pentripplanner/api/parameter/QualifiedModeSet.java 0.00% 3 Missing ⚠️
.../graph_builder/module/DirectTransferGenerator.java 91.17% 1 Missing and 2 partials ⚠️
...raptoradapter/transit/mappers/TransfersMapper.java 71.42% 2 Missing ⚠️
...t/java/org/opentripplanner/ext/flex/FlexIndex.java 75.00% 1 Missing ⚠️
.../java/org/opentripplanner/ext/flex/FlexRouter.java 0.00% 1 Missing ⚠️
...n/java/org/opentripplanner/model/PathTransfer.java 87.50% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             dev-2.x    #6293      +/-   ##
=============================================
+ Coverage      69.79%   69.82%   +0.03%     
- Complexity     17788    17842      +54     
=============================================
  Files           2017     2022       +5     
  Lines          76042    76303     +261     
  Branches        7781     7804      +23     
=============================================
+ Hits           53074    53281     +207     
- Misses         20264    20306      +42     
- Partials        2704     2716      +12     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Comment on lines 137 to 145
if (pathTransfer == null) {
EnumSet<StreetMode> modes = EnumSet.of(mode);
distinctTransfers.put(
transferKey,
new PathTransfer(stop, sd.stop, sd.distance, sd.edges, modes)
);
} else {
pathTransfer.addMode(mode);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could add a comment here to explain what these different cases mean.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added comments for the different cases

}
// Calculate flex transfers if flex routing is enabled.
for (RouteRequest transferProfile : flexTransferRequests) {
StreetMode mode = transferProfile.journey().transfer().mode();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this just always WALK?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have always assumed that flex transfers can only be on foot. I think this is reasonable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this to StreetMode mode = StreetMode.WALK;

@@ -250,6 +250,61 @@ public void testMultipleRequestsWithPatterns() {
);
}

@Test
public void testPathTransfersWithModesForMultipleRequestsWithPatterns() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does multiple requests with patterns mean?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to follow a similar naming convention as the other tests in the file. With patterns means that trippatterns are added to the timetablerepository of the test. Multiple requests means that transfers are generated for multiple requests that have different modes:

var reqWalk = new RouteRequest();
reqWalk.journey().transfer().setMode(StreetMode.WALK);

var reqBike = new RouteRequest();
reqBike.journey().transfer().setMode(StreetMode.BIKE);

var transferRequests = List.of(reqWalk, reqBike);

this.toStop = toStop;
this.edges = edges;
this.distanceMeters = (int) edges.stream().mapToDouble(Edge::getDistanceMeters).sum();
this.modes = modes;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably want to make this immutable. EnumSet are not.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the getModes method to clone the enum set

Comment on lines 277 to 291
var walkTransfers = timetableRepository
.getAllPathTransfers()
.stream()
.filter(pathTransfer -> pathTransfer.getModes().contains(StreetMode.WALK))
.toList();
var bikeTransfers = timetableRepository
.getAllPathTransfers()
.stream()
.filter(pathTransfer -> pathTransfer.getModes().contains(StreetMode.BIKE))
.toList();
var carTransfers = timetableRepository
.getAllPathTransfers()
.stream()
.filter(pathTransfer -> pathTransfer.getModes().contains(StreetMode.CAR))
.toList();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please extract a method

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created a getTransfersByMode method in TimetableRepository

@leonardehrenfried
Copy link
Member

  • The solution is to use CAR transfers only with relevant stops.

Does this mean that this PR allows you to compute CAR transfers between stops that don't serve cars at all?

@@ -41,6 +43,7 @@ public static RaptorTransferIndex create(
var transfers = transfersByStopIndex
.get(fromStop)
.stream()
.filter(transfer -> transfer.getModes().contains(mode))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is run only once and then cached for the specific combination of request parameters, correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is run once for each cache request. You can see the code that uses this in RaptorRequestTransferCache

return this.modes;
}

public boolean addMode(StreetMode mode) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want to make this class mutable. Can you create a copy method, for example withAddedMode that returns a new instance instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this to use the withAddedMode method

@@ -46,6 +56,14 @@ public List<Edge> getEdges() {
return this.edges;
}

public EnumSet<StreetMode> getModes() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want to expose the mutable EnumSet. Can you replace it with a method like allows(CAR)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the getModes method to clone the enum set

Comment on lines 211 to 212
.map(transferProfile -> transferProfile.journey().transfer().mode())
.collect(Collectors.toSet())) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.map(transferProfile -> transferProfile.journey().transfer().mode())
.collect(Collectors.toSet())) {
.map(transferProfile -> transferProfile.journey().transfer().mode())
.distinct()
.toList()

Not a huge deal but avoiding a set gives you predictable sort order.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is a good suggestion. I changed it to this

@VillePihlava
Copy link
Contributor Author

Does this mean that this PR allows you to compute CAR transfers between stops that don't serve cars at all?

Yes, in a future PR (#6215) I will implement more configurability for transfers

}

public EnumSet<StreetMode> getModes() {
return modes.clone();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return modes.clone();
return EnumSet.copyOf(modes);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this

this.toStop = toStopIndex;
this.distanceMeters = distanceMeters;
this.edges = null;
this.modes = modes;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This transfer is not serialized. Here you can use Sets.immutableEnumSet and then never have to worry about cloning or copying.

Copy link
Contributor Author

@VillePihlava VillePihlava Dec 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this to use Sets.immutableEnumSet

Comment on lines 212 to 222
for (StreetMode mode : transferRequests
.stream()
.map(transferProfile -> transferProfile.journey().transfer().mode())
.distinct()
.toList()) {
LOG.info(
"Created {} transfers for mode {}.",
transfersByStop
.values()
.stream()
.filter(pathTransfer -> pathTransfer.getModes().contains(mode))
.count(),
mode
);
Copy link
Member

@optionsome optionsome Dec 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is quite difficult to read. I would probably continue the stream instead of making it a list and then looping over it, and then call some method with the mode as an argument that does the logging. So something like:

transferRequests
      .stream()
      .map(transferProfile -> transferProfile.journey().transfer().mode())
      .distinct()
      .peek(mode -> logTransfersForMode(mode));

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it to this:

transferRequests
      .stream()
      .map(transferProfile -> transferProfile.journey().transfer().mode())
      .distinct()
      .forEach(mode ->
        LOG.info(
          "Created {} transfers for mode {}.",
          timetableRepository.getTransfersByMode(mode).size(),
          mode
        )
      );

@miklcct
Copy link
Contributor

miklcct commented Dec 10, 2024

I am following this as I am now testing BICYCLE routing on our deployment. The current deployment configuration only has "walk" with PT15M in build-config.json and the bicycle routing is near-useless for cross-London journeys, as it can't return results cycling from one London terminal to another (some of these transfers require about 30 minutes on the bike).

However, if I set the build-config.json to do walk and bike transfers with PT30M duration, the graph has become 15.1 GB just for London only without any other parts of Great Britain, and the transfer cache takes forever to build rather than seconds that it can't even cope with a city-sized deployment, and I can't even think of how it can work when put on a country-wide deployment.

@@ -434,7 +435,7 @@ public Collection<PathTransfer> getTransfersByStop(StopLocation stop) {
return transfersByStop.get(stop);
}

public Collection<PathTransfer> getTransfersByMode(StreetMode mode) {
public List<PathTransfer> getTransfersByMode(StreetMode mode) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this method should be called findTransfers according to the naming conventions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed getTransfersByMode to findTransfers

optionsome
optionsome previously approved these changes Dec 12, 2024
@leonardehrenfried
Copy link
Member

@t2gran can we run the speed tests against that? Do you know how to do it?

@@ -31,16 +35,20 @@ public class Transfer {

private final List<Edge> edges;

public Transfer(int toStop, List<Edge> edges) {
private final ImmutableSet<StreetMode> modes;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know there was a lot of back and forth with this. In the end @t2gran says he didn't want Guava collection, so lets use Collections.unmodifiableSet.

Suggested change
private final ImmutableSet<StreetMode> modes;
private final Set<StreetMode> modes;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason we can do it here is that it's not serialized.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it to this

this.toStop = toStop;
this.edges = edges;
this.distanceMeters = (int) edges.stream().mapToDouble(Edge::getDistanceMeters).sum();
this.modes = Sets.immutableEnumSet(modes);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.modes = Sets.immutableEnumSet(modes);
this.modes = Collections.unmodifiableSet(modes);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it to this

this.toStop = toStopIndex;
this.distanceMeters = distanceMeters;
this.edges = null;
this.modes = Sets.immutableEnumSet(modes);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.modes = Sets.immutableEnumSet(modes);
this.modes = Collections.unmodifiableSet(modes);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it to this

@@ -68,6 +76,10 @@ public List<Edge> getEdges() {
return edges;
}

public ImmutableSet<StreetMode> getModes() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't actually need to expose the collection at all. Simply add a method allowsMode(mode). So you don't even have to think about if it's mutable or immutable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also document the method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed getModes to allowsMode

@@ -433,6 +435,14 @@ public Collection<PathTransfer> getTransfersByStop(StopLocation stop) {
return transfersByStop.get(stop);
}

public List<PathTransfer> findTransfers(StreetMode mode) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add Javadoc here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

/**
* Maps street mode to transfer traverse mode.
*/
public class StreetModeToTransferTraverseModeMapper {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice that you extraced the mapper.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks

@leonardehrenfried
Copy link
Member

I also ran the speed test on this branch and I cannot see any changes: https://tinyurl.com/27zv22uz

Screenshot From 2024-12-13 14-45-45

Performance-wise this is fine.

return EnumSet.copyOf(modes);
}

public PathTransfer withAddedMode(StreetMode mode) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Javadoc please.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

@@ -68,6 +75,10 @@ public List<Edge> getEdges() {
return edges;
}

public boolean allowsMode(StreetMode mode) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Javadoc here and then we are good.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll fix this next week. Btw does this need the bump serialization id label?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. You are storing a new field into the graph.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now has a comment

@leonardehrenfried leonardehrenfried added the bump serialization id Add this label if you want the serialization id automatically bumped after merging the PR label Dec 13, 2024
@optionsome optionsome merged commit 19c2804 into opentripplanner:dev-2.x Dec 16, 2024
5 checks passed
@optionsome optionsome deleted the split-transfers-by-mode-pathtransfer-mode branch December 16, 2024 11:49
t2gran pushed a commit that referenced this pull request Dec 16, 2024
t2gran pushed a commit that referenced this pull request Dec 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bump serialization id Add this label if you want the serialization id automatically bumped after merging the PR Improvement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants